package jmine.tec.rpc.common.serialization.json;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

import jmine.tec.json.JSON;
import jmine.tec.json.JSONNullObject;
import jmine.tec.json.JSONObject;
import jmine.tec.json.JSONString;
import jmine.tec.rpc.RPCMessages;
import jmine.tec.rpc.common.RPCException;

/**
 * Contexto de conversão JSON, lida com reentrâncias para tratar corretamente referências circulares.
 * 
 * @author lundberg
 */
public class JSONConversionContext {

    private static final String TYPE = "&type";

    private static final JSON TYPE_VALUE = new JSONString("reference");

    private static final String REFID = "&refId";

    private final JSONConverter converter;

    private Long referenceCounter = 0L;

    private final Map<Object, JSON> javaJSONMap = new IdentityHashMap<Object, JSON>();

    private final Map<JSON, Object> jsonJavaMap = new HashMap<JSON, Object>();

    /**
     * Construtor
     * 
     * @param converter converter
     */
    public JSONConversionContext(JSONConverter converter) {
        this.converter = converter;
    }

    /**
     * Converte o objeto em JSON
     * 
     * @param object object
     * @return json
     */
    public JSON toJSON(Object object) {
        if (this.javaJSONMap.containsKey(object)) {
            return this.referenceJSON(object);
        } else {
            return this.converter.toJSON(this, object);
        }
    }

    /**
     * Verifica se o JSON pode ser referenciado
     * 
     * @param json json
     * @return boolean
     */
    protected boolean canBeReferenced(JSON json) {
        return json instanceof JSONObject;
    }

    /**
     * Cria um JSON que é referência a outro json convertido previamente
     * 
     * @param object object
     * @return JSON
     */
    private JSON referenceJSON(Object object) {
        JSONObject json = new JSONObject();
        json.setProperty(TYPE, TYPE_VALUE);
        json.setProperty(REFID, this.getRefId(object));
        return json;
    }

    /**
     * Obtém o id de referência ao objeto
     * 
     * @param object object
     * @return referência
     */
    private JSON getRefId(Object object) {
        JSON json = this.javaJSONMap.get(object);
        if (!this.hasReference(json)) {
            json.setProperty(REFID, new JSONString(Long.toString(this.referenceCounter++)));
        }
        return json.getProperty(REFID);
    }

    /**
     * Converte o JSON em objeto java
     * 
     * @param json json
     * @return object
     */
    public Object toJava(JSON json) {
        if (this.isReference(json)) {
            Object object = this.jsonJavaMap.get(json.getProperty(REFID));
            if (object == null) {
                throw new RPCException(RPCMessages.REFERENCE_FOUND_BEFORE_OBJECT.create(json.getProperty(REFID)));
            }
            return object;
        } else {
            return this.converter.toJava(this, json);
        }
    }

    /**
     * Verifica se o JSON dado é uma referência
     * 
     * @param json json
     * @return boolean
     */
    private boolean isReference(JSON json) {
        return json instanceof JSONObject && TYPE_VALUE.equals(json.getProperty(TYPE));
    }

    /**
     * Verifica se o JSON dado é uma referência
     * 
     * @param json json
     * @return boolean
     */
    private boolean hasReference(JSON json) {
        return !JSONNullObject.INSTANCE.equals(json.getProperty(REFID));
    }

    /**
     * Armazena uma associação entre o objeto JSON e um objeto Java
     * 
     * @param json json
     * @param object object
     */
    public void storeReference(JSON json, Object object) {
        this.javaJSONMap.put(object, json);
        if (this.hasReference(json)) {
            this.jsonJavaMap.put(json.getProperty(REFID), object);
        }
    }
}
